package dkbta.ontology.loading;

import static dkbta.ontology.elements.ElementType.STAT;
import static dkbta.ontology.elements.ElementType.SUBJECTSTAT;
import static dkbta.ontology.loading.OntologyParser.parseDurationCondition;
import static dkbta.ontology.loading.OntologyParser.parseDurationValues;
import static dkbta.ontology.loading.OntologyParser.parseNumericValues;
import static dkbta.ontology.loading.OntologyParser.parseValueConsdition;
import static dkbta.util.Utils.enumForName;
import static dkbta.util.Utils.isEmpty;

import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import dkbta.ontology.Ontology;
import dkbta.ontology.elements.ElementType;
import dkbta.ontology.elements.pattern.PatternDef;
import dkbta.ontology.elements.pattern.PatternElement;
import dkbta.ontology.elements.pattern.conditions.BeforeTemporalCondition;
import dkbta.ontology.elements.pattern.conditions.BoundPointCondition;
import dkbta.ontology.elements.pattern.conditions.DurationCondition;
import dkbta.ontology.elements.pattern.conditions.OverlappingTemporalCondition;
import dkbta.ontology.elements.pattern.conditions.PairWiseCondition;
import dkbta.ontology.elements.pattern.conditions.TemporalCondition;
import dkbta.ontology.loading.OntologyParser.ElementParser;
import dkbta.ontology.values.DurationValues;
import dkbta.ontology.values.NumericValues;
import dkbta.ontology.values.ValueCondition;
import dkbta.util.Utils;
import dkbta.util.time.TimeUnit;

final class PatternParser implements ElementParser{
	private final Set<ElementType> _invalidPatternElementTypes = EnumSet.of(STAT,
		SUBJECTSTAT);

	private final Ontology _ontology;

	private Map<String, Integer> _elementNamesToOrdinals;


	public PatternParser(Ontology ontology){
		_ontology = ontology;
		_elementNamesToOrdinals = new HashMap<String, Integer>();
	}

	@Override
	public ElementType getElementType(){
		return ElementType.PATTERN;
	}

	public void parseElement(Element pattern, String name, String componentType)
			throws OntologyException{
		Collection<PatternElement> elements = parsePatternElements(pattern);
		Collection<PairWiseCondition> pwcs = parsePairWiseConditions(pattern);
		_ontology.addPattern(new PatternDef(name, componentType, pwcs, elements));
	}

	private Collection<PairWiseCondition> parsePairWiseConditions(Element pattern)
			throws OntologyException{
		NodeList pwcNodes = pattern.getElementsByTagName("PairWiseCondition");
		int length = pwcNodes.getLength();
		Collection<PairWiseCondition> pwcs = new ArrayList<PairWiseCondition>(length);
		int numOfPatternElements = _elementNamesToOrdinals.size();
		Set<Integer> encounteredOrdinals = new HashSet<Integer>(numOfPatternElements);
		for (int i = 0; i < length; ++i){
			Element pwcElement = (Element)pwcNodes.item(i);

			try{
				PairWiseCondition pwc = parsePairWiseCondition(pwcElement);
				pwcs.add(pwc);

				// Noting the elements that have a PWC associated with them
				encounteredOrdinals.add(pwc.getFirst());
				encounteredOrdinals.add(pwc.getSecond());
			}catch(OntologyException e){
				throw new OntologyException("Unable to parse pair-wise condition #"
						+ (i + 1), e);
			}
		}

		_elementNamesToOrdinals.values().removeAll(encounteredOrdinals);
		if (!_elementNamesToOrdinals.isEmpty()){
			throw new OntologyException("The following elements have no "
					+ "pair-wise conditions associated with them: "
					+ _elementNamesToOrdinals.keySet());
		}

		if (pwcs.isEmpty()){
			throw new OntologyException("There are no pair-wise conditions");
		}
		return pwcs;
	}

	private PairWiseCondition parsePairWiseCondition(Element pwcElement)
			throws OntologyException{
		String firstElementName = pwcElement.getAttribute("firstElementName");
		String secondElementName = pwcElement.getAttribute("secondElementName");
		Integer firstElementOrdinal = _elementNamesToOrdinals.get(firstElementName);
		Integer secondElementOrdinal = _elementNamesToOrdinals.get(secondElementName);
		if (firstElementOrdinal == null){
			throw new OntologyException("First element is unknown: " + firstElementName);
		}
		if (secondElementOrdinal == null){
			throw new OntologyException("Second element is unknown: " + secondElementName);
		}

		String temporalConditionStr = pwcElement.getAttribute("temporalCondition");
		TemporalCondition.Type tcType = enumForName(TemporalCondition.Type.class,
			temporalConditionStr);
		TemporalCondition tc;
		switch (tcType){
			case BEFORE:
				DurationValues validGapDuration;
				try{
					validGapDuration = parseDurationValues(pwcElement, "minGap", "maxGap");
				}catch(OntologyException e){
					throw new OntologyException(
							"Invalid minGap/maxGap value range on the gap of a BEFORE temporal condition",
							e);
				}
				tc = new BeforeTemporalCondition(validGapDuration);
				break;
			case OVERLAPPING:
				DurationValues validOverlapDuration,
				validStartingDistance;
				try{
					validOverlapDuration = parseDurationValues(pwcElement, "minOverlap",
						"maxOverlap");
				}catch(OntologyException e){
					throw new OntologyException(
							"Invalid minOverlap/maxOverlap value range on "
									+ "the overlap of an OVERLAPPING temporal condition",
							e);
				}
				try{
					validStartingDistance = OntologyParser.parseDurationValues(
						pwcElement, "minStartingDistance", "maxStartingDistance");
				}catch(OntologyException e){
					throw new OntologyException(
							"Invalid minStartingDistance/maxStartingDistance "
									+ "value range on the starting distance of the second "
									+ "element from the first element of an OVERLAPPING temporal condition",
							e);
				}
				tc = new OverlappingTemporalCondition(validOverlapDuration,
						validStartingDistance);
				break;
			default:
				throw new OntologyException("Unhandled temporal condition type: "
						+ tcType);
		}

		return new PairWiseCondition(firstElementOrdinal, secondElementOrdinal, tc);
	}

	private Collection<PatternElement> parsePatternElements(Element pattern)
			throws OntologyException{
		NodeList elementNodes = pattern.getElementsByTagName("Element");
		int length = elementNodes.getLength();
		Collection<PatternElement> elements = new ArrayList<PatternElement>(length);
		for (int ordinal = 0; ordinal < length; ++ordinal){
			Element peElement = (Element)elementNodes.item(ordinal);

			try{
				PatternElement pe = parsePatternElement(ordinal, peElement);
				elements.add(pe);
			}catch(OntologyException e){
				throw new OntologyException("Unable to parse pattern element #"
						+ (ordinal + 1), e);
			}

		}
		if (elements.isEmpty()){
			throw new OntologyException("There are no pattern elements");
		}
		return elements;
	}

	private PatternElement parsePatternElement(int ordinal, Element peElement)
			throws OntologyException{
		// Validating that the element name exists and is not a duplicate
		String patternElementName = peElement.getAttribute("name");
		if (isEmpty(patternElementName)){
			throw new OntologyException("Lacks an element name");
		}
		if (_elementNamesToOrdinals.containsKey(patternElementName)){
			throw new OntologyException("Duplicate element name '" + patternElementName
					+ "', same as #"
					+ (_elementNamesToOrdinals.get(patternElementName) + 1));
		}
		_elementNamesToOrdinals.put(patternElementName, ordinal);

		// Obtaining the ontology element node
		Element ontElement = getOntologyElement(peElement);
		String patternElementTypeStr = ontElement.getTagName();

		// Parsing the ontology element the pattern is based on
		ElementType et = enumForName(ElementType.class, patternElementTypeStr);
		if (_invalidPatternElementTypes.contains(et)){
			throw new OntologyException("Invalid pattern element type: " + et);
		}

		String ontElementName = ontElement.getAttribute("name");
		if (Utils.isEmpty(ontElementName)){
			throw new OntologyException("Invalid ontology element name: "
					+ ontElementName);
		}

		// Parsing the local conditions
		ValueCondition vc = parseValueConsdition(ontElement, patternElementTypeStr,
			_invalidPatternElementTypes, true);
		BoundPointCondition bpc = parseBoundPointCondition(ontElement);
		DurationCondition dc = parseDurationCondition(ontElement);

		if (vc == null && bpc == null && dc == null){
			throw new OntologyException("No conditions defined on the element");
		}

		return new PatternElement(patternElementName, ordinal, ontElementName, et, vc,
				dc, bpc);
	}

	private Element getOntologyElement(Element peElement) throws OntologyException{
		NodeList childNodes = peElement.getChildNodes();

		int elementNodeCount = 0;
		Element ontElement = null;
		for (int i = 0; i < childNodes.getLength(); ++i){
			Node node = childNodes.item(i);
			if (node.getNodeType() == Node.ELEMENT_NODE){
				elementNodeCount++;
				ontElement = (Element)node;
			}
		}

		if (elementNodeCount != 1){
			throw new OntologyException("Invalid number of ontology elements: "
					+ elementNodeCount);
		}
		return ontElement;
	}

	private BoundPointCondition parseBoundPointCondition(Element ontElement)
			throws OntologyException{
		Element bpcElement = OntologyParser.getXmlElement(ontElement,
			"BoundpointCondition");
		if (bpcElement == null){
			return null;
		}
		Element startPtE = OntologyParser.getXmlElement(bpcElement, "StartPoint");
		Element endPtE = OntologyParser.getXmlElement(bpcElement, "EndPoint");
		if (startPtE == null && endPtE == null){
			return null;
		}

		TimeUnit startTU = null, endTU = null;
		NumericValues startV = null, endV = null;
		try{
			if (startPtE != null){
				startTU = enumForName(TimeUnit.class, startPtE.getAttribute("timeUnit"));
				startV = parseNumericValues(startPtE, false, null);
				if (startV == null){
					throw new OntologyException("Missing numeric value range");
				}
			}
		}catch(Exception e){
			throw new OntologyException("Invalid start point in boundpoint condition", e);
		}

		try{
			if (endPtE != null){
				endTU = enumForName(TimeUnit.class, endPtE.getAttribute("timeUnit"));
				endV = parseNumericValues(endPtE, false, null);
				if (endV == null){
					throw new OntologyException("Missing numeric value range");
				}
			}
		}catch(Exception e){
			throw new OntologyException("Invalid end point in boundpoint condition", e);
		}

		return new BoundPointCondition(startV, startTU, endV, endTU);
	}
}